{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercise 2 - Solution"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's where we ended up after completing Exercise 1:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from json import JSONEncoder, dumps\n",
    "from datetime import date, datetime\n",
    "from decimal import Decimal\n",
    "\n",
    "class CustomEncoder(JSONEncoder):\n",
    "    def default(self, obj):\n",
    "        if isinstance(obj, Stock) or isinstance(obj, Trade):\n",
    "            result =  obj.as_dict()\n",
    "            result['object'] = obj.__class__.__name__\n",
    "            return result\n",
    "        elif isinstance(obj, datetime):\n",
    "            return obj.strftime('%Y-%m-%dT%H:%M:%S')\n",
    "        elif isinstance(obj, date):\n",
    "            return obj.strftime('%Y-%m-%d')\n",
    "        elif isinstance(obj, Decimal):\n",
    "            return str(obj)\n",
    "        else:\n",
    "            super().default(obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Stock:\n",
    "    def __init__(self, symbol, date_, open_, high, low, close, volume):\n",
    "        self.symbol = symbol\n",
    "        self.date = date_\n",
    "        self.open = open_\n",
    "        self.high = high\n",
    "        self.low = low\n",
    "        self.close = close\n",
    "        self.volume = volume\n",
    "        \n",
    "    def as_dict(self):\n",
    "        return dict(symbol=self.symbol, \n",
    "                    date=self.date,\n",
    "                    open=self.open,\n",
    "                    high=self.high,\n",
    "                    low=self.low,\n",
    "                    close=self.close,\n",
    "                    volume=self.volume)\n",
    "        \n",
    "class Trade:\n",
    "    def __init__(self, symbol, timestamp, order, price, volume, commission):\n",
    "        self.symbol = symbol\n",
    "        self.timestamp = timestamp\n",
    "        self.order = order\n",
    "        self.price = price\n",
    "        self.commission = commission\n",
    "        self.volume = volume\n",
    "        \n",
    "    def as_dict(self):\n",
    "        return dict(\n",
    "            symbol=self.symbol,\n",
    "            timestamp=self.timestamp,\n",
    "            order=self.order,\n",
    "            price=self.price,\n",
    "            volume=self.volume,\n",
    "            commission=self.commission)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "activity = {\n",
    "    \"quotes\": [\n",
    "        Stock('TSLA', date(2018, 11, 22), \n",
    "              Decimal('338.19'), Decimal('338.64'), Decimal('337.60'), Decimal('338.19'), 365_607),\n",
    "        Stock('AAPL', date(2018, 11, 22), \n",
    "              Decimal('176.66'), Decimal('177.25'), Decimal('176.64'), Decimal('176.78'), 3_699_184),\n",
    "        Stock('MSFT', date(2018, 11, 22), \n",
    "              Decimal('103.25'), Decimal('103.48'), Decimal('103.07'), Decimal('103.11'), 4_493_689)\n",
    "    ],\n",
    "    \n",
    "    \"trades\": [\n",
    "        Trade('TSLA', datetime(2018, 11, 22, 10, 5, 12), 'buy', Decimal('338.25'), 100, Decimal('9.99')),\n",
    "        Trade('AAPL', datetime(2018, 11, 22, 10, 30, 5), 'sell', Decimal('177.01'), 20, Decimal('9.99'))\n",
    "    ]\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we could serialize our objects:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "  \"quotes\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"338.19\",\n",
      "      \"high\": \"338.64\",\n",
      "      \"low\": \"337.60\",\n",
      "      \"close\": \"338.19\",\n",
      "      \"volume\": 365607,\n",
      "      \"object\": \"Stock\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"176.66\",\n",
      "      \"high\": \"177.25\",\n",
      "      \"low\": \"176.64\",\n",
      "      \"close\": \"176.78\",\n",
      "      \"volume\": 3699184,\n",
      "      \"object\": \"Stock\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"MSFT\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"103.25\",\n",
      "      \"high\": \"103.48\",\n",
      "      \"low\": \"103.07\",\n",
      "      \"close\": \"103.11\",\n",
      "      \"volume\": 4493689,\n",
      "      \"object\": \"Stock\"\n",
      "    }\n",
      "  ],\n",
      "  \"trades\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"timestamp\": \"2018-11-22T10:05:12\",\n",
      "      \"order\": \"buy\",\n",
      "      \"price\": \"338.25\",\n",
      "      \"volume\": 100,\n",
      "      \"commission\": \"9.99\",\n",
      "      \"object\": \"Trade\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"timestamp\": \"2018-11-22T10:30:05\",\n",
      "      \"order\": \"sell\",\n",
      "      \"price\": \"177.01\",\n",
      "      \"volume\": 20,\n",
      "      \"commission\": \"9.99\",\n",
      "      \"object\": \"Trade\"\n",
      "    }\n",
      "  ]\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "encoded = dumps(activity, cls=CustomEncoder, indent=2)\n",
    "print(encoded)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we want to reverse the process and deserialize this JSON object. I am not going to assume any specific schema other than `Stock` and `Trade` objects will contain the `\"class\": \"Stock\"` or `\"object\": \"Trade\"` entries and the required additional fields to define those objects."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "What I want to do is examine each dictionary, and if it contains those entries, I will want to deserialize as the corresponding objects.\n",
    "\n",
    "We'll need to pay attention also to `date`, `datetime`, and `Decimal` type objects."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's start by writing a utility function that will convert a JSON dictionary of each specific type to the corresponding object type:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "def decode_stock(d):\n",
    "    # assumes \"class\": \"Stock\" is in the dictionary\n",
    "    # and contains all the required serialized fields needed to re-create the object\n",
    "    # if working in Python 3.7, we could use date.fromisoformat(d['date']) instead\n",
    "    s = Stock(d['symbol'], \n",
    "              datetime.strptime(d['date'], '%Y-%m-%d').date(), \n",
    "              Decimal(d['open']), \n",
    "              Decimal(d['high']), \n",
    "              Decimal(d['low']), \n",
    "              Decimal(d['close']),\n",
    "              int(d['volume']))\n",
    "    return s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's make sure this works:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = decode_stock({\n",
    "      \"symbol\": \"AAPL\",\n",
    "      \"date\": \"2018-11-22\",\n",
    "      \"open\": \"176.66\",\n",
    "      \"high\": \"177.25\",\n",
    "      \"low\": \"176.64\",\n",
    "      \"close\": \"176.78\",\n",
    "      \"volume\": 3699184,\n",
    "      \"object\": \"Stock\"\n",
    "    })"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(__main__.Stock,\n",
       " {'symbol': 'AAPL',\n",
       "  'date': datetime.date(2018, 11, 22),\n",
       "  'open': Decimal('176.66'),\n",
       "  'high': Decimal('177.25'),\n",
       "  'low': Decimal('176.64'),\n",
       "  'close': Decimal('176.78'),\n",
       "  'volume': 3699184})"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(s), vars(s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's do the same thing with a `Trade`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def decode_trade(d):\n",
    "    # assumes \"class\": \"Trade\" is in the dictionary\n",
    "    # and contains all the required serialized fields needed to re-create the object\n",
    "    s = Trade(d['symbol'], \n",
    "              datetime.strptime(d['timestamp'], '%Y-%m-%dT%H:%M:%S'), \n",
    "              d['order'], \n",
    "              Decimal(d['price']), \n",
    "              int(d['volume']), \n",
    "              Decimal(d['commission']))\n",
    "    return s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = decode_trade({\n",
    "      \"symbol\": \"TSLA\",\n",
    "      \"timestamp\": \"2018-11-22T10:05:12\",\n",
    "      \"order\": \"buy\",\n",
    "      \"price\": \"338.25\",\n",
    "      \"volume\": 100,\n",
    "      \"commission\": \"9.99\",\n",
    "      \"object\": \"Trade\"\n",
    "    })"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(__main__.Trade,\n",
       " {'symbol': 'TSLA',\n",
       "  'timestamp': datetime.datetime(2018, 11, 22, 10, 5, 12),\n",
       "  'order': 'buy',\n",
       "  'price': Decimal('338.25'),\n",
       "  'commission': Decimal('9.99'),\n",
       "  'volume': 100})"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(t), vars(t)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, these look good to go, so one last utility function that can take in **either** a `Stock` or `Trade` type JSON object, and decode accordingly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def decode_financials(d):\n",
    "    object_type = d.get('object', None)\n",
    "    if object_type == 'Stock':\n",
    "        return decode_stock(d)\n",
    "    elif object_type == 'Trade':\n",
    "        return decode_trade(d)\n",
    "    return d  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<__main__.Trade at 0x10c4dc1d0>"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "decode_financials({\n",
    "      \"symbol\": \"TSLA\",\n",
    "      \"timestamp\": \"2018-11-22T10:05:12\",\n",
    "      \"order\": \"buy\",\n",
    "      \"price\": \"338.25\",\n",
    "      \"volume\": 100,\n",
    "      \"commission\": \"9.99\",\n",
    "      \"object\": \"Trade\"\n",
    "    })"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<__main__.Stock at 0x10c4dc588>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "decode_financials({\n",
    "      \"symbol\": \"AAPL\",\n",
    "      \"date\": \"2018-11-22\",\n",
    "      \"open\": \"176.66\",\n",
    "      \"high\": \"177.25\",\n",
    "      \"low\": \"176.64\",\n",
    "      \"close\": \"176.78\",\n",
    "      \"volume\": 3699184,\n",
    "      \"object\": \"Stock\"\n",
    "    })"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So now let's write our custom JSON decoding class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from json import JSONDecoder, loads"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CustomDecoder(JSONDecoder):\n",
    "    def decode(self, arg):\n",
    "        data = loads(arg)\n",
    "        # now we have to recursively look for `Trade` and `Stock` objects\n",
    "        return self.parse_financials(data)\n",
    " \n",
    "    def parse_financials(self, obj):\n",
    "        if isinstance(obj, dict):\n",
    "            obj = decode_financials(obj)\n",
    "            if isinstance(obj, dict):\n",
    "                for key, value in obj.items():\n",
    "                    obj[key] = self.parse_financials(value)\n",
    "        elif isinstance(obj, list):\n",
    "            for index, item in enumerate(obj):\n",
    "                obj[index] = self.parse_financials(item)\n",
    "        return obj"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's recall our serialized data first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "  \"quotes\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"338.19\",\n",
      "      \"high\": \"338.64\",\n",
      "      \"low\": \"337.60\",\n",
      "      \"close\": \"338.19\",\n",
      "      \"volume\": 365607,\n",
      "      \"object\": \"Stock\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"176.66\",\n",
      "      \"high\": \"177.25\",\n",
      "      \"low\": \"176.64\",\n",
      "      \"close\": \"176.78\",\n",
      "      \"volume\": 3699184,\n",
      "      \"object\": \"Stock\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"MSFT\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"103.25\",\n",
      "      \"high\": \"103.48\",\n",
      "      \"low\": \"103.07\",\n",
      "      \"close\": \"103.11\",\n",
      "      \"volume\": 4493689,\n",
      "      \"object\": \"Stock\"\n",
      "    }\n",
      "  ],\n",
      "  \"trades\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"timestamp\": \"2018-11-22T10:05:12\",\n",
      "      \"order\": \"buy\",\n",
      "      \"price\": \"338.25\",\n",
      "      \"volume\": 100,\n",
      "      \"commission\": \"9.99\",\n",
      "      \"object\": \"Trade\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"timestamp\": \"2018-11-22T10:30:05\",\n",
      "      \"order\": \"sell\",\n",
      "      \"price\": \"177.01\",\n",
      "      \"volume\": 20,\n",
      "      \"commission\": \"9.99\",\n",
      "      \"object\": \"Trade\"\n",
      "    }\n",
      "  ]\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "print(encoded)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "decoded = loads(encoded, cls=CustomDecoder)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'quotes': [<__main__.Stock at 0x10c4df550>,\n",
       "  <__main__.Stock at 0x10c4b6400>,\n",
       "  <__main__.Stock at 0x10c4b6be0>],\n",
       " 'trades': [<__main__.Trade at 0x10c4b6e48>, <__main__.Trade at 0x10c4b6978>]}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "decoded"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "How can we check of the two objects are \"equal\"? The problem is that we did not define equality for `Stock` and `Trade` objects, so we cannot compare two instances of the same class and expect equality even if they have the same data. We need to define that first!\n",
    "Let's do that:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Stock:\n",
    "    def __init__(self, symbol, date_, open_, high, low, close, volume):\n",
    "        self.symbol = symbol\n",
    "        self.date = date_\n",
    "        self.open = open_\n",
    "        self.high = high\n",
    "        self.low = low\n",
    "        self.close = close\n",
    "        self.volume = volume\n",
    "        \n",
    "    def as_dict(self):\n",
    "        return dict(symbol=self.symbol, \n",
    "                    date=self.date,\n",
    "                    open=self.open,\n",
    "                    high=self.high,\n",
    "                    low=self.low,\n",
    "                    close=self.close,\n",
    "                    volume=self.volume)\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        return isinstance(other, Stock) and self.as_dict() == other.as_dict()\n",
    "        \n",
    "class Trade:\n",
    "    def __init__(self, symbol, timestamp, order, price, volume, commission):\n",
    "        self.symbol = symbol\n",
    "        self.timestamp = timestamp\n",
    "        self.order = order\n",
    "        self.price = price\n",
    "        self.commission = commission\n",
    "        self.volume = volume\n",
    "        \n",
    "    def as_dict(self):\n",
    "        return dict(\n",
    "            symbol=self.symbol,\n",
    "            timestamp=self.timestamp,\n",
    "            order=self.order,\n",
    "            price=self.price,\n",
    "            volume=self.volume,\n",
    "            commission=self.commission)\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        return isinstance(other, Trade) and self.as_dict() == other.as_dict()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "activity = {\n",
    "    \"quotes\": [\n",
    "        Stock('TSLA', date(2018, 11, 22), \n",
    "              Decimal('338.19'), Decimal('338.64'), Decimal('337.60'), Decimal('338.19'), 365_607),\n",
    "        Stock('AAPL', date(2018, 11, 22), \n",
    "              Decimal('176.66'), Decimal('177.25'), Decimal('176.64'), Decimal('176.78'), 3_699_184),\n",
    "        Stock('MSFT', date(2018, 11, 22), \n",
    "              Decimal('103.25'), Decimal('103.48'), Decimal('103.07'), Decimal('103.11'), 4_493_689)\n",
    "    ],\n",
    "    \n",
    "    \"trades\": [\n",
    "        Trade('TSLA', datetime(2018, 11, 22, 10, 5, 12), 'buy', Decimal('338.25'), 100, Decimal('9.99')),\n",
    "        Trade('AAPL', datetime(2018, 11, 22, 10, 30, 5), 'sell', Decimal('177.01'), 20, Decimal('9.99'))\n",
    "    ]\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "encoded = dumps(activity, cls=CustomEncoder)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "decoded = loads(encoded, cls=CustomDecoder)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "decoded == activity"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
